
package Individual;

import Loader.BufferedData;
import Loader.Pair;
import Settings.Settings;
import java.util.Iterator;

public class FitnessFunctions {   

    private BufferedData bufferedData;


    public FitnessFunctions(BufferedData bufferedData) {

        this.bufferedData = bufferedData;       

    }


    public float fitness( Individual individual, boolean learn) {

        individual.computeCentroids();

        float result = 0;

        switch(Settings.FITNESS_FUNCTION_TYPE) {

            case 0: //euclides
                result = euclidesFitness( individual, learn );
                break;

            case 1: //DBi
                result = dbiFitness( individual, learn );
                break;

            case 2: //F rate
                result = fRateFitenss( individual, learn );
                break;

        }

        return result;

    }

    private float euclidesFitness( Individual individual, boolean learn ) {

        float result = 0;
        Iterator<Pair> it;

        if(learn)
            it = bufferedData.dataLearn.iterator();
        else
            it = bufferedData.dataTest.iterator();

        // compute all distances only if they were never yet computed
        // this situation happens either in first generation or for test data
        if(individual.storedDistances.get(0).get(0) == null || !learn) {

            int dataCounter = 0;

            while( it.hasNext() ) {

                Pair pair = it.next();

                float fitMin = Float.MAX_VALUE;

                for(int c = 0; c < individual.numOfClusters; ++c) {

                    float fitTemp = 0;

                    //features for cluster
                    for(int f = 0; f < individual.numOfFeatures; ++f) {

                        fitTemp += Math.pow(individual.chromosomeVal.get(c).get(f) -
                                            pair.features[f], 2);

                    }

                    if( fitTemp < fitMin )
                        fitMin = fitTemp;

                    individual.storedDistances.get(c).set(dataCounter, (float) Math.sqrt(fitTemp)); // <---- to trzeba jeszcze sprawdzic

                }

                dataCounter++;

                result += Math.sqrt(fitMin);

            }

//            return (result != 0) ? (1/result) : Float.MAX_VALUE;
        }

        // otherwise check which centers have changed, and compute new distances
        // only for them
        else {

            for(int cluster=0; cluster < individual.numOfClusters; cluster++) {

                // if this cluster has changed, compute distances for all records
                if(individual.centroidChangedFlags.get(cluster)) {

                    int dataCounter = 0;

                    while( it.hasNext() ) {

                        Pair pair = it.next();
                        float fitTemp = 0;

                        //features for cluster
                        for(int f = 0; f < individual.numOfFeatures; ++f) {
                            fitTemp += Math.pow(individual.chromosomeVal.get(cluster).get(f) -
                                                pair.features[f], 2);
                        }

                        individual.storedDistances.get(cluster).set(dataCounter, (float) Math.sqrt(fitTemp)); // <---- to trzeba jeszcze sprawdzic                       
                    }
                }
            }

            // now find each record's closest cluster
            for(int data = 0 ; data < individual.storedDistances.size(); data++) {

                float fitMin = Float.MAX_VALUE;
                float fitTemp = Float.MAX_VALUE;

                // find the cluster, to which this data point lies closest
                for(int i=0; i<individual.numOfClusters; i++) {

                    fitTemp = individual.storedDistances.get(i).get(data);

                    if( fitTemp < fitMin )
                        fitMin = fitTemp;
                }

                // no sqrt! the distance is stored already in correct way
                result += fitMin;//individual.storedDistances.get(cluster).get(data); // <---- to trzeba jeszcze sprawdzic

            }

            
        }

        return (result != 0) ? (1/result) : Float.MAX_VALUE;
        
    }

    private float dbiFitness( Individual individual, boolean learn ) {

        float result = 0;

        //measure of scatter within the clusters
        float[] S = new float[individual.numOfClusters];

        //counters for clusters
        int[] countersCluster = new int[individual.numOfClusters];

        //measure of separation between cluster
        float[] M = new float[individual.numOfClusters];

        Iterator<Pair> it;
        if(learn)
            it = bufferedData.dataLearn.iterator();
        else
            it = bufferedData.dataTest.iterator();


        // compute all distances only if they were never yet computed
        // this situation happens either in first generation or for test data
        if(individual.storedDistances.get(0).get(0) == null || !learn) {

            int dataCounter = 0;

            while( it.hasNext() ) {

                Pair pair = it.next();
                float fitMin = Float.MAX_VALUE;
                int clusterSelected = 0;

                for(int c = 0; c < individual.numOfClusters; ++c) {

                    float fitTemp = 0;

                    //features for cluster
                    for(int f = 0; f < individual.numOfFeatures; ++f) {

                        fitTemp += Math.pow(individual.chromosomeVal.get(c).get(f) -
                                            pair.features[f], 2);

                    }

                    individual.storedDistances.get(c).set(dataCounter, (float) Math.sqrt(fitTemp));

                    if( fitTemp < fitMin ) {

                        fitMin = fitTemp;
                        clusterSelected = c;
                    }                    
                }

                S[clusterSelected] += Math.sqrt(fitMin);
                countersCluster[clusterSelected]++;

                dataCounter++;
            }            
        }
        // otherwise check which centers have changed, and compute new distances
        // only for them
        else {

            for(int cluster=0; cluster < individual.numOfClusters; cluster++) {

                // if this cluster has changed, compute distances for all records
                if(individual.centroidChangedFlags.get(cluster)) {

                    int dataCounter = 0;

                    while( it.hasNext() ) {

                        Pair pair = it.next();
                        float fitTemp = 0;

                        //features for cluster
                        for(int f = 0; f < individual.numOfFeatures; ++f) {
                            fitTemp += Math.pow(individual.chromosomeVal.get(cluster).get(f) -
                                                pair.features[f], 2);
                        }

                        individual.storedDistances.get(cluster).set(dataCounter, (float) Math.sqrt(fitTemp)); // <---- to trzeba jeszcze sprawdzic
                    }
                }
            }

            // now find each record's closest cluster
            for(int data = 0 ; data < individual.storedDistances.size(); data++) {

                float fitMin = Float.MAX_VALUE;
                float fitTemp = Float.MAX_VALUE;
                int clusterSelected = 0;

                // find the cluster, to which this data point lies closest
                for(int c=0; c<individual.numOfClusters; c++) {

                    fitTemp = individual.storedDistances.get(c).get(data);

                    if( fitTemp < fitMin ) {
                        fitMin = fitTemp;
                        clusterSelected = c;
                    }
                }

                S[clusterSelected] += fitMin;
                countersCluster[clusterSelected]++;

            }



        }


        //scatters within the clusters
        for(int i = 0; i < individual.numOfClusters; ++i)
            if( countersCluster[i] != 0)
                S[i] = (float) Math.sqrt( S[i] / countersCluster[i] );
            else
                S[i] = 0;

        //separation 2 clusters: c1 and c2
        for(int c1 = 0; c1 + 1 < individual.numOfClusters; ++c1)
            for(int c2 = c1 + 1; c2 < individual.numOfClusters; ++c2) {

                float Mt = S[c1] + S[c2];

                //distance beetwen c1 and c2
                float distance = 0;
                //features for cluster
                for(int f = 0; f < individual.numOfFeatures; ++f) {

                    float feature1 = individual.chromosomeVal.get(c1).get(f);

                    float feature2 = individual.chromosomeVal.get(c2).get(f);

                    distance += Math.pow( feature1 - feature2, 2);

                }

                if(distance != 0 && Mt != 0)
                    Mt /= Math.sqrt(distance);
                else
                    Mt = Float.MAX_VALUE;

                if(M[c1] < Mt)
                    M[c1] = Mt;

                if(M[c2] < Mt)
                    M[c2] = Mt;

            }

        for(int i = 0; i < individual.numOfClusters; ++i)
            result += M[i];

        result /= individual.numOfClusters;

        if(result == Float.POSITIVE_INFINITY)
            result = Float.MAX_VALUE;

        return (result != 0) ? (1/result) : Float.MAX_VALUE;

    }

    private float fRateFitenss(Individual individual, boolean learn) {

        int[][] countersClusters = new int[Settings.CHROMOSOME_NUMBER_OF_CLUSTERS]
                                          [Settings.NUMBER_OF_CLASS];

        Iterator<Pair> it;
        if(learn)
            it = bufferedData.dataLearn.iterator();
        else
            it = bufferedData.dataTest.iterator();

        //count how many records belong to classes in clusters for best individ.
        while( it.hasNext() ) {

            Pair pair = it.next();
            float fitMin = Float.MAX_VALUE;
            int clusterSelected = 0;

            for(int c = 0; c < Settings.CHROMOSOME_NUMBER_OF_CLUSTERS; ++c) {

                float fitTemp = 0;

                //features for cluster
                for(int f = 0; f < Settings.CHROMOSOME_NUMBER_OF_FEATURES; ++f){

                    fitTemp += Math.pow(individual.chromosomeVal.get(c).get(f) -
                                        pair.features[f], 2);

                }

                if( fitTemp < fitMin ) {

                    fitMin = fitTemp;
                    clusterSelected = c;
                }

            }

            countersClusters[clusterSelected][pair.noClass-1]++;

        }

        //compute TP(m=0), FP(m=1), FN(m=2), TN(m=3) measures for each class
        double[][] measures = new double[Settings.NUMBER_OF_CLASS][4];
        for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
            for(int m = 0; m < 4; ++m)
                measures[i][m] = 0.0f;

        for(int c = 0; c < Settings.CHROMOSOME_NUMBER_OF_CLUSTERS; ++c) {

            //find what class is represented by cluster c
            int noClassInCluster = -1;
            int maxRecordsInClass = 0;
            for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
                if(maxRecordsInClass < countersClusters[c][i]) {
                    maxRecordsInClass = countersClusters[c][i];
                    noClassInCluster = i;
                }

            //cluster is empty
            if(noClassInCluster == -1)
                continue;

            //compute measure
            boolean errorNoClass = false;
            for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
                if( maxRecordsInClass == countersClusters[c][i] &&
                    i != noClassInCluster
                  ) {

                    errorNoClass = true;
                    measures[i][2] += countersClusters[c][i];//FN

                }

            if(errorNoClass) {

                measures[noClassInCluster][2] += maxRecordsInClass;//FN
                for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
                    if( i != noClassInCluster &&
                        countersClusters[c][i] < maxRecordsInClass
                      )
                        measures[i][3] += maxRecordsInClass;//TN

            } else {

                measures[noClassInCluster][0] += maxRecordsInClass;//TP
                for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
                    if(i != noClassInCluster)
                        measures[i][3] += maxRecordsInClass;//TN

            }

            for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i)
                if( countersClusters[c][i] < maxRecordsInClass &&
                    countersClusters[c][i] > 0
                  ) {

                  measures[i][2] += countersClusters[c][i];//FN
                  measures[noClassInCluster][1] += countersClusters[c][i];//FP

                  for(int j = 0; j < Settings.NUMBER_OF_CLASS; ++j)
                      if(j != i && j != noClassInCluster)
                          measures[j][3] += countersClusters[c][i];//TN
                }
        }

        //check sum measures in each class if they're equal
        double sumMeasures = measures[0][0] +
                             measures[0][1] +
                             measures[0][2] +
                             measures[0][3];
        double sumMeasuresTemp;
        for(int i = 1; i < Settings.NUMBER_OF_CLASS; ++i) {
            sumMeasuresTemp = measures[i][0] +
                              measures[i][1] +
                              measures[i][2] +
                              measures[i][3];
            if(sumMeasuresTemp != sumMeasures) {
                System.out.println(String.format("Error in compute measures! " +
                                                 "In class %d: " +
                                                 "Sum measures=%.5f & " +
                                                 "Sum measures temp=%.5f",(i+1),
                                                 sumMeasures, sumMeasuresTemp));
                return 0.0f;
            }
        }

        //compute rate for each class
        double f = 0.0f;
        double precision;
        double recall;
        for(int i = 0; i < Settings.NUMBER_OF_CLASS; ++i) {
            if(measures[i][0] > 0) {
                precision = measures[i][0] / (measures[i][0] + measures[i][1]);
                recall = measures[i][0] / (measures[i][0] + measures[i][2]);
            } else {
                precision = 0;
                recall = 0;
            }

            if(precision != 0 || recall != 0)
                f += 2 * ( (precision*recall) / (precision+recall) );
        }
        return (float) (f / Settings.NUMBER_OF_CLASS);

    }

}
